#
# Copyright 2009 Red Hat, Inc. and/or its affiliates.
#
# Licensed to you under the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.  See the files README and
# LICENSE_GPL_v2 which accompany this distribution.
#
"""
Metadata module provides generic metadata class with common verification and
validation functionality implemented.
"""

# XXX This is deprecated in Python 2.5
# If/when this code will be migrated to more up to date python this should be
# changed to hashlib instead of sha
import sha
import logging
import rwlock
import misc

import storage_exception as se

SHA_CKSUM_TAG = "_SHA_CKSUM"

class Metadata(object):
    """
    Metadata class provides interface for a generic set of key=value pairs
    that can be accessed by any consumer
    """
    log = logging.getLogger("irs")
    metalog = log #logging.getLogger("irs.metadata")

    def __init__(self, maxDataSize=None, allowedlist = None):
        self.__metadata = None
        self.__cacheable = True
        self.__allowedparams = allowedlist
        self.__lock = rwlock.RWLock()
        self._maxDataSize = maxDataSize

    def _get(self):
        """
        Metadata extractor - needs to be implemented by the heir.
        Extracts the metadata from the stable storage
        """
        pass

    def _set(self, md):
        """
        Metadata committer - needs to be implemented by the heir.
        Commits the metadata 'md' to the stable storage
        """
        pass

    def _validate(self, md):
        """
        Metadata validator - needs to be implemented by heir.
        Performs implementation specific metadata validation
        """
        pass
    def __cksum(self, md):
        """
        Calculates the checksum of the metadata
        """
        md.sort()
        #self.log.debug("metadata.__cksum() net metadata %s", md)
        cksum = sha.new()
        for item in md:
            cksum.update(item)
        #self.log.debug("metadata.__cksum() sha=%s", cksum.hexdigest())
        return cksum.hexdigest()

    def __validate(self, md):
        """
        Validates the metadata 'md',
        as a side effect removes the checksum tag from the metadata
        """
        cksum = computed_cksum = 0

        originalmd = md[:]
        for item in originalmd:
            if SHA_CKSUM_TAG in item:
                cksum = item.split("=")[1]
                md.remove(item)

        if cksum != 0:
            # There was a checksum in the metadata, validate it
            computed_cksum = self.__cksum(md)
            self.log.debug("metadata.__validate() __cksum(md) = %s", computed_cksum)
            if not (cksum == computed_cksum):
                self.metalog.warning("Metadata seal is broken (%s != %s)\n"
                    "Original metadata is '%s' ('%s')", cksum, computed_cksum, originalmd, md)
                raise se.MetaDataSealIsBroken(cksum, computed_cksum)
        else:
            # No checksum in the metadata, let it through as is
            self.log.debug("metadata.__validate() Metadata has no embedded "
                "checksum - trust it as it is")

        return self._validate(md)


    def __getmetadata(self):
        """
        Metadata accessor
        """
        self.__lock.rlock()
        # If md is cached and up to date
        try:
            if self.__metadata and self.__cacheable:
                # Return a copy, not the list itself
                return self.__metadata[:]
        finally:
                self.__lock.runlock()

        return self.__refreshCache()

    @misc.samplingmethod
    def __refreshCache(self):
        self.__lock.wlock()
        try:
            try:
                md = self._get()
                self.__validate(md)
            except se.MetaDataSealIsBroken:
                # Make sure that this isn't a transient error (i.e. reread data from disk and validate it)
                self.metalog.warning("metadata.__getmetadata: retry getting the MD")
                md = self._get()
                self.__validate(md)

            if self.__cacheable:
                # Set the copy aside as a cache for future access
                self.__metadata = md[:]

            return md
        finally:
                self.__lock.wunlock()


    def __setmetadata(self, md):
        """
        Metadata mutator
        """
        self.__lock.wlock()
        try:
            # set metadata to be equal to md
            tmpmd = md[:]
            tmpmd.append("%s=%s" % (SHA_CKSUM_TAG, self.__cksum(md)))

            if (not self._maxDataSize is None) and (self._maxDataSize < len(tmpmd)):
                raise se.MetadataOverflowError()

            self._set(tmpmd)
            # Cache should be updated only AFTER write to disk succeeds
            if self.__cacheable:
                self.__metadata = md[:]
        finally:
            self.__lock.wunlock()

    def clearcache(self):
        """
        Flush metadata cache if any
        """
        self.__lock.wlock()
        try:
            self.__metadata = None
        finally:
            self.__lock.wunlock()

    metadata = property(__getmetadata, __setmetadata, doc="Actual metadata")


def main():
    m = Metadata()
    dir(m)

if __name__ == "__main__":
    main()
